
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

using Swaf;
using Swaf.BizRule;

namespace Swaf.FtpServer
{
	/// <summary>
	/// Summary description for FtpSession.
	/// </summary>
	public class FtpSession
	{
		protected IApplication m_app;
		protected IAppManagement m_appMgr;
		protected Swaf.BGuid.Guid m_sessionId;
		protected Thread m_sessionThread;
		protected FtpServer m_server;
		protected Socket m_clientSocket;

		protected TcpListener m_passiveListener;
		protected Socket m_dataSocket;
		protected IPEndPoint m_dataConnectEndPoint;

		protected int m_badCmdCount = 0;
		protected string m_workingDirectory;

		protected static char[] s_cmdSplits = new char[]{' '};
			
		public FtpSession(FtpServer server, Socket clientSocket)
		{
			m_server = server;
			m_clientSocket = clientSocket;
			m_workingDirectory = @"\";

			m_appMgr = (IAppManagement)Application.currentApp;
			m_sessionId = new Swaf.BGuid.Guid();
			m_app = m_appMgr.createSessionApplication(m_sessionId.ToString());

			m_sessionThread = new Thread(new ThreadStart(processSession));
#if(!COMPACT_FRAMEWORK)
			m_sessionThread.IsBackground = true;
#endif
			m_sessionThread.Start();
		}

		protected internal Socket ClientSocket {get{return m_clientSocket;}}
		protected internal TcpListener PassiveListener
		{
			get{return m_passiveListener;}
			set{m_passiveListener = value;}
		}
		protected internal IPEndPoint DataConnectEndPoint
		{
			get{return m_dataConnectEndPoint;}
			set{m_dataConnectEndPoint = value;}
		}

		public Swaf.BGuid.Guid SessionId {get{return m_sessionId;}}

		public string CurrentDirectory 
		{
			get
			{
				return m_workingDirectory.Replace(@"\", "/");
			}
			set
			{
				string curDir = value;

				// If conatins \, replace with / 
				curDir = curDir.Replace("/",@"\");

				// If doesn't end with /, append it
				if(!curDir.EndsWith(@"\"))
					curDir += @"\";

			
				//--- Normalize path, eg. /ivx/../test must be /ivx/test
				curDir = curDir.Replace("..", "");
				curDir = curDir.Replace(@"\\",@"\");

				m_workingDirectory = curDir;
			}
		}

		public FtpServer Server {get{return m_server;}}

		public string calcFullFileName(string partialName)
		{
			string name = RealDirectory + partialName;
			// If conatins \, replace with / 
			name = name.Replace("/",@"\");
			
			//--- Normalize path, eg. /ivx/../test must be /ivx/test
			name = name.Replace("..", "");
			name = name.Replace(@"\\",@"\");

			return name;
		}

		public string RealDirectory {get{return m_server.BaseDirectory + m_workingDirectory;}}

		private void processSession()
		{
			DateTime lastClientCmd = DateTime.Now;
			string lastCmd;
			try
			{
				if(m_server.isClientAllowedToConnect(m_clientSocket.RemoteEndPoint))
				{
					sendToClient("220 FTP server ready\r\n");
					while(true)
					{
						if(m_clientSocket.Available > 0)
						{
							try
							{
								lastCmd = readClientLine();
								if(!processCommand(lastCmd) && processBadCommand(null))
									break;
							}
							catch(Exception e)
							{
								if(processBadCommand(e))
									break;
							}
							finally
							{
								if(m_dataSocket != null)
								{
									m_dataSocket.Shutdown(SocketShutdown.Both);
									m_dataSocket.Close();
									m_dataSocket = null;
								}
							}

							// reset last command time
							lastClientCmd = DateTime.Now;
						}
						else
						{
							//Session timeout support
							if(DateTime.Now.Ticks > lastClientCmd.Ticks + ((long)(m_server.IdleTimeOut)) * 10000)
							{
								// Notify for closing
								ftpError("500 Session timeout, OK FTP server signing off\r\n");
								break;
							}
							
							// Wait to save CPU, otherwise while loop may take 100% CPU. 
							Thread.Sleep(250);							
						}
					}
				}
				else
				{
				}

			}
			finally
			{
				m_server.sessionEnded(this);
				if(m_clientSocket.Connected)
					m_clientSocket.Close();
			}
		}

		protected internal Socket getDataConnection()
		{
			if(m_dataSocket == null)
			{
				try
				{
					if(PassiveListener != null)
					{
						// Wait ftp client connection
						long startTime = DateTime.Now.Ticks;
						// Wait ftp server to connect
						while(!PassiveListener.Pending())
						{
							System.Threading.Thread.Sleep(50);

							// Time out after 30 seconds
							if((DateTime.Now.Ticks - startTime) / 10000 > 20000)
							{
								throw new FtpException("Ftp server didn't respond !");
							}
						}
						m_dataSocket = PassiveListener.AcceptSocket();
						sendToClient("125 Data connection open, Transfer starting.\r\n");
					}
					else
					{
						sendToClient("150 Opening data connection.\r\n");
						m_dataSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
						//TODO: m_dataConnectEndPoint could be null and will blowup here.  Need to setup the value
						//based on something else, but what?
						m_dataSocket.Connect(m_dataConnectEndPoint);
					}
				}
				catch
				{
					sendToClient("425 Can't open data connection.\r\n");
					return null;
				}

			}

			return m_dataSocket;
		}
		

		protected bool processBadCommand(Exception e)
		{
			if(m_badCmdCount > m_server.MaxBadCommands-1)
			{
				sendToClient("421 Too many bad commands, closing transmission channel\r\n");
				return true;
			}
			++m_badCmdCount;

			if(e is FtpException)
				ftpError("500 " + e.Message);
			else
			{
				// Connection lost
				if(!m_clientSocket.Connected)
					return true;
								
				ftpError("500 Unkown temp error\r\n");
			}
			return false;
		}

		protected bool processCommand(string fullLine)
		{
			string command = "", commandArgs = "";
			int pos = fullLine.IndexOf(' ');
			if(pos != -1)
			{
				command  = fullLine.Substring(0, pos).ToUpper();
				commandArgs = fullLine.Substring(pos +1).TrimStart();
			}
			else
				command = fullLine.ToUpper();

			StringBuilder buff = new StringBuilder();
			buff.Append("Ftp").Append(command).Append("CommandProcessor");
			IResults rc = m_app.ruleMgr.execute(buff.ToString(), commandArgs, this);
			return (rc.ExceptionInfo == null || (rc.CallResults is bool && (bool)rc.CallResults == false));
		}

		protected internal void ftpError(string errorMsg)
		{
			sendToClient(errorMsg);
		}

		protected internal void sendToClient(string data)
		{
			sendToClient(data, false);
		}

		protected internal void sendToClient(string data, bool useDataConnectSocket)
		{
			Socket s = useDataConnectSocket?getDataConnection():m_clientSocket;
			Byte[] byte_data = System.Text.Encoding.ASCII.GetBytes(data.ToCharArray());
			int size = s.Send(byte_data,byte_data.Length,0);
		}

		protected internal void sendToClient(byte[] data, bool useDataConnectSocket)
		{
			Socket s = useDataConnectSocket?getDataConnection():m_clientSocket;
			int size = s.Send(data);
		}

		protected string readClientLine()
		{
			try
			{
				long lastDataTime   = DateTime.Now.Ticks;
				ArrayList lineBuf   = new ArrayList();
				byte      prevByte  = 0;
				
				while(true)
				{
					if(m_clientSocket.Available > 0)
					{
						// Read next byte
						byte[] currByte = new byte[1];
						int countRecieved = m_clientSocket.Receive(currByte,1, SocketFlags.None);
						// Count must be equal. Eg. some computers won't give byte at first read attempt
						if(countRecieved == 1)
						{
							lineBuf.Add(currByte[0]);

							// Line found
							if((prevByte == (byte)'\r' && currByte[0] == (byte)'\n'))
							{
								byte[] retVal = new byte[lineBuf.Count-2];    // Remove <CRLF> 
								lineBuf.CopyTo(0,retVal,0,lineBuf.Count-2);
							
								return System.Text.Encoding.Default.GetString(retVal).Trim();
							}
						
							// Store byte
							prevByte = currByte[0];

							// Check if maximum length is exceeded
							if(lineBuf.Count > m_server.MaxLineLength)
							{
								throw new FtpReadException("Maximum line length exceeded");
							}

							// reset last data time
							lastDataTime = DateTime.Now.Ticks;
						}						
					}
					else
					{
						//---- Time out stuff -----------------------//
						if(DateTime.Now.Ticks > lastDataTime + ((long)(m_server.IdleTimeOut)) * 10000)
						{
							throw new FtpReadException("Read timeout");
						}					
						System.Threading.Thread.Sleep(100);									
						//------------------------------------------//
					}
				}
			}
			catch(FtpException e)
			{
				throw e;
			}
			catch(Exception e)
			{
				throw new FtpReadException(e.Message);
			}
		}
	}
}
